package current.limiting.service.impl;

import current.limiting.annotions.Limiting;
import current.limiting.commom.enumerate.LimitType;
import current.limiting.service.CurrentLimitStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

import static current.limiting.commom.constant.CurrentLimitConstant.FIXED_TIME_REDIS_KEY;
import static current.limiting.commom.constant.CurrentLimitConstant.UNDERSCORE;
import static current.limiting.commom.enumerate.DealType.FAST_FAIL;
import static current.limiting.commom.enumerate.LimitType.QPS;
import static current.limiting.utils.LimitUtil.time2Millisecond;
import static org.cleaver.spring.utils.servlet.ServletUtil.request;

/**
 * 固定时间(通过Redis实现) 接口限流策略
 * <p>
 * 该接口从第一次被访问开始计时，在指定的时间段内仅支持最大给定数量的请求访问；当给定的时间段后的第一次访问将会重置计时
 *
 * @author LiKun
 */
@Service
public class FixedTimeRedisCurrentLimitService implements CurrentLimitStrategy {
    private static final Logger logger = LoggerFactory.getLogger(FixedTimeRedisCurrentLimitService.class);

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public boolean isAllowAccess(String id, Limiting limiting) throws InterruptedException {
        // redis 中存放数据的Key值，并将其放入到字符串常量池中
        String key = this.getKey(id, limiting.limitType());
        // 获取操作 Redis 中给定key值的 BoundKeyOperations 对象
        BoundValueOperations valueOps = redisTemplate.boundValueOps(key);
        // expire：Key的存活时间；blockTime：阻塞时间
        long expire, blockTime = time2Millisecond(limiting.blockTime(), limiting.timeUnit());

        // 当单位时间内允许通过的请求数量 > 0 && 当前时间段已通过的请求数量 < 允许通过的请求数量
        if (limiting.accessQuantity() > 0 && judgeCurrentInterfaceReachedMaxVisits(limiting, valueOps)) {
            if (FAST_FAIL == limiting.dealType()) {
                logger.info("线程{}不被允许访问接口(FAST_FAIL.FastFail)", Thread.currentThread().getName());
                // 如果接口限流策略为 FAST_FAIL，则直接返回失败
                return false;
            } else {
                // 此处不需要唤醒等待的线程，因为已经为线程设置了等待的时间
                synchronized (key.intern()) {
                    while (judgeCurrentInterfaceReachedMaxVisits(limiting, valueOps)) {
                        // 如果此时存放通过数据量的 Redis.key 刚好失效，则使请求直接通过
                        if ((expire = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS)) >= 0) {
                            if (limiting.blockTime() == 0) {                                // 首先判断该接口是否一直阻塞
                                logger.info("线程{}不被允许访问接口(BLOCKING.Blocking)，expire：{}", Thread.currentThread().getName(), expire);
                                Thread.sleep(expire);
                            } else {
                                // 判断剩余的等待时间是否大于Key的存活时间
                                if (blockTime <= expire) {
                                    logger.info("线程{}不被允许访问接口(BLOCKING.FastFail)，blockTime：{}，expire：{}", Thread.currentThread().getName(), blockTime, expire);
                                    // 快速失败；当用户设置的阻塞时间 小于 Key值的存活时间
                                    return false;
                                } else {
                                    logger.info("线程{}不被允许访问接口(BLOCKING.Blocking)，blockTime：{}，expire：{}", Thread.currentThread().getName(), blockTime, expire);
                                    blockTime = blockTime - expire;
                                    Thread.sleep(expire);                       // 如果同一个接口的前一个请求无法通过，则其后的请求在该时间段内也一定无法通过
                                }
                            }
                        }
                    }
                }
            }
        }

        return callBackHandler(id, key, blockTime, limiting);
    }

    @Override
    public boolean callBackHandler(String id, String key, long blockTime, Limiting limiting) throws InterruptedException {
        return true;
    }

    private String getKey(String id, LimitType limitType) {
        String key = FIXED_TIME_REDIS_KEY.concat(id);
        return QPS == limitType ? key.intern() : key.concat(UNDERSCORE).concat(request().getRemoteAddr()).intern();
    }

    /**
     * 判断当前请求访问的接口在本段时间内是否已达到最大访问量
     *
     * @param limiting 接口限流设置
     * @param valueOps redis 操作接口
     *
     * @return 当前接口是否达到最大访问量
     */
    private boolean judgeCurrentInterfaceReachedMaxVisits(Limiting limiting, BoundValueOperations valueOps) {
        long accessedQuantity;
        if ((accessedQuantity = valueOps.increment()) == 1L) {
            valueOps.expire(limiting.limitTime(), limiting.timeUnit());
        }
        return accessedQuantity > limiting.accessQuantity();
    }
}
